--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit e8e5128bc67cd3e8540bcd9f4e9be1b8d43db36a
Parents : eb0fec5
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-03-31T00:32:22+03:00
feat(ci): add scripts for cosign setup, release asset attestation, and verification
Changes
5 files changed, 169 insertions(+), 0 deletions(-)
Diff
diff --git a/cosign.pub b/cosign.pub
new file mode 100644
index 00000000..1007711e
--- /dev/null
+++ b/cosign.pub
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyWx2eajLfc+pflz3cq2XcmopUoO9
+9oGHYfmtd+zSod22RkU4YJtIcEBesql7Wb+wjkqBxjpXgdrTB9Tu3dPVZQ==
+-----END PUBLIC KEY-----
diff --git a/scripts/ci/attest-release-assets.sh b/scripts/ci/attest-release-assets.sh
new file mode 100755
index 00000000..3a759474
--- /dev/null
+++ b/scripts/ci/attest-release-assets.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+# Create SLSA v1 cosign bundle attestations next to each release binary under DIR.
+# Requires: cosign on PATH; COSIGN_KEY_PATH to cosign private key PEM; COSIGN_PASSWORD
+# if the key is encrypted. Run from repository root so scripts/ci/slsa-predicate.py resolves.
+#
+# Usage: attest-release-assets.sh <directory>
+set -eu
+
+DIR="${1:?directory}"
+KEY="${COSIGN_KEY_PATH:?set COSIGN_KEY_PATH}"
+
+if [ ! -f "$KEY" ]; then
+ echo "attest-release-assets.sh: missing key file $KEY" >&2
+ exit 1
+fi
+
+PRED="$(mktemp "${TMPDIR:-/tmp}/slsa-pred.XXXXXX")"
+trap 'rm -f "$PRED"' EXIT INT
+
+python3 scripts/ci/slsa-predicate.py > "$PRED"
+
+find "$DIR" -type f ! -name '*.sha256' ! -name '*.cosign.bundle' | while IFS= read -r f; do
+ case "$f" in
+ */.git/*) continue ;;
+ esac
+ echo "attest: $f"
+ cosign attest-blob --yes \
+ --key "$KEY" \
+ --predicate "$PRED" \
+ --type slsaprovenance1 \
+ --bundle "${f}.cosign.bundle" \
+ --tlog-upload=true \
+ "$f" >/dev/null
+done
+
+echo "attest-release-assets.sh: done"
diff --git a/scripts/ci/setup-cosign.sh b/scripts/ci/setup-cosign.sh
new file mode 100755
index 00000000..9966c6fd
--- /dev/null
+++ b/scripts/ci/setup-cosign.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+# Install cosign from GitHub releases with SHA256 verification.
+# Usage: setup-cosign.sh [version]
+set -eu
+
+COSIGN_VERSION="${1:-3.0.5}"
+
+ARCH="$(uname -m)"
+case "$ARCH" in
+ x86_64) ARCH="amd64" ;;
+ aarch64) ARCH="arm64" ;;
+ *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;;
+esac
+
+BASE_URL="https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}"
+BINARY="cosign-linux-${ARCH}"
+CHECKSUMS_URL="${BASE_URL}/cosign_checksums.txt"
+
+curl -fsSL "$CHECKSUMS_URL" -o /tmp/cosign-checksums.txt
+curl -fsSL "${BASE_URL}/${BINARY}" -o /tmp/cosign
+
+EXPECTED="$(grep " ${BINARY}\$" /tmp/cosign-checksums.txt | awk '{print $1}')"
+ACTUAL="$(sha256sum /tmp/cosign | awk '{print $1}')"
+if [ -z "$EXPECTED" ] || [ "$EXPECTED" != "$ACTUAL" ]; then
+ echo "SHA256 verification failed for ${BINARY}" >&2
+ rm -f /tmp/cosign /tmp/cosign-checksums.txt
+ exit 1
+fi
+
+sudo install -m 0755 /tmp/cosign /usr/local/bin/cosign
+rm -f /tmp/cosign /tmp/cosign-checksums.txt
+cosign version
diff --git a/scripts/ci/slsa-predicate.py b/scripts/ci/slsa-predicate.py
new file mode 100644
index 00000000..9ae3e51d
--- /dev/null
+++ b/scripts/ci/slsa-predicate.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""Emit a SLSA v1 provenance predicate JSON on stdout (stdin unused)."""
+from __future__ import annotations
+
+import json
+import os
+from datetime import datetime, timezone
+
+
+def _source_uri() -> str:
+ server = (os.environ.get("GITHUB_SERVER_URL") or os.environ.get("GITEA_SERVER_URL") or "").rstrip("/")
+ repo = os.environ.get("GITHUB_REPOSITORY") or os.environ.get("GITEA_REPOSITORY") or ""
+ if not server or not repo:
+ return ""
+ if server.startswith("https://") or server.startswith("http://"):
+ return f"git+{server}/{repo}.git"
+ return f"git+https://{server}/{repo}.git"
+
+
+def _build_type() -> str:
+ custom = os.environ.get("PROVENANCE_BUILD_TYPE")
+ if custom:
+ return custom
+ server = (os.environ.get("GITHUB_SERVER_URL") or os.environ.get("GITEA_SERVER_URL") or "").rstrip("/")
+ repo = os.environ.get("GITHUB_REPOSITORY") or os.environ.get("GITEA_REPOSITORY") or ""
+ if server and repo:
+ return f"{server}/{repo}/.gitea/workflows/build.yml"
+ return "https://slsa.dev/provenance/v1"
+
+
+def _builder_id() -> str:
+ custom = os.environ.get("PROVENANCE_BUILDER_ID")
+ if custom:
+ return custom
+ server = (os.environ.get("GITHUB_SERVER_URL") or os.environ.get("GITEA_SERVER_URL") or "").rstrip("/")
+ if server:
+ return f"{server}/actions"
+ return "https://gitea.io/actions/runner"
+
+
+def main() -> None:
+ ref = os.environ.get("GITHUB_REF", "")
+ sha = os.environ.get("GITHUB_SHA", "")
+ run_id = os.environ.get("GITHUB_RUN_ID", "")
+ attempt = os.environ.get("GITHUB_RUN_ATTEMPT", "1")
+ workflow = os.environ.get("GITHUB_WORKFLOW", "")
+ started = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ internal = {}
+ if workflow:
+ internal["workflow"] = workflow
+
+ predicate = {
+ "buildDefinition": {
+ "buildType": _build_type(),
+ "externalParameters": {
+ "source": _source_uri(),
+ "ref": ref,
+ "revision": sha,
+ },
+ "internalParameters": internal,
+ "resolvedDependencies": [],
+ },
+ "runDetails": {
+ "builder": {"id": _builder_id()},
+ "metadata": {
+ "invocationId": f"{run_id}-{attempt}",
+ "startedOn": started,
+ },
+ },
+ }
+ print(json.dumps(predicate, separators=(",", ":")))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/ci/verify-release-attestation.sh b/scripts/ci/verify-release-attestation.sh
new file mode 100755
index 00000000..d7f9388e
--- /dev/null
+++ b/scripts/ci/verify-release-attestation.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+# Verify a cosign SLSA bundle for a release binary using the repository public key.
+# Checks Sigstore Rekor (public log) unless COSIGN_REKOR_URL points elsewhere.
+# Usage: verify-release-attestation.sh <blob-file> <bundle-file>
+# Env: COSIGN_PUBLIC_KEY (default cosign.pub)
+set -eu
+
+BLOB="${1:?blob path}"
+BUNDLE="${2:?bundle path}"
+PUB="${COSIGN_PUBLIC_KEY:-cosign.pub}"
+
+if [ ! -f "$PUB" ]; then
+ echo "Missing $PUB (generate a key pair with cosign and commit the .pub file)" >&2
+ exit 1
+fi
+
+exec cosign verify-blob-attestation \
+ --key "$PUB" \
+ --bundle "$BUNDLE" \
+ --type slsaprovenance1 \
+ "$BLOB"
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────